package com.samphanie.dries.controller.rest;

import com.samphanie.dries.domain.Auth;
import com.samphanie.dries.domain.MfaType;
import com.samphanie.dries.domain.User;
import com.samphanie.dries.domain.dto.LoginDto;
import com.samphanie.dries.domain.dto.SendTotpDto;
import com.samphanie.dries.domain.dto.TotpVerificationDto;
import com.samphanie.dries.domain.dto.UserDto;
import com.samphanie.dries.exception.*;
import com.samphanie.dries.service.IEmailService;
import com.samphanie.dries.service.ISmsService;
import com.samphanie.dries.service.UserCacheService;
import com.samphanie.dries.service.UserService;
import com.samphanie.dries.util.JwtUtil;
import com.samphanie.dries.util.OptionalEx;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.context.MessageSource;
import org.springframework.data.util.Pair;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.nio.file.AccessDeniedException;
import java.util.Locale;
import java.util.Optional;

/**
 * @Description:
 * @Author ZSY
 * @createTime 2021/2/25 14:23
 */
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/authorize")
public class AuthorizeResource {

    private final UserService userService;
    private final UserCacheService userCacheService;
    private final ISmsService smsService;
    private final IEmailService emailService;
    private final MessageSource messageSource;
    private final JwtUtil jwtUtil;

    @GetMapping("/validation/username")
    public boolean validateUsername(@RequestParam String username) {
        return userService.isUsernameExisted(username);
    }

    @GetMapping("/validation/email")
    public boolean validateEmail(@RequestParam String email) {
        return userService.isEmailExisted(email);
    }

    @GetMapping("/validation/mobile")
    public boolean validateMobile(@RequestParam String mobile) {
        return userService.isMobileExisted(mobile);
    }


    @PostMapping("/register")
    public void register(@Valid @RequestBody UserDto userDto, Locale locale) {
        if (userService.isUsernameExisted(userDto.getUsername())) {
            throw new DuplicateProblem("Exception.duplicate.username", messageSource, locale);
        }
        if (userService.isEmailExisted(userDto.getEmail())) {
            throw new DuplicateProblem("Exception.duplicate.email", messageSource, locale);
        }
        if (userService.isMobileExisted(userDto.getMobile())) {
            throw new DuplicateProblem("Exception.duplicate.mobile", messageSource, locale);
        }
        val user = User.builder()
                .username(userDto.getUsername())
                .nickname(userDto.getNickname())
                .email(userDto.getEmail())
                .mobile(userDto.getMobile())
                .password(userDto.getPassword())
                .build();
        userService.register(user);

    }

    @PostMapping("/token")
    public ResponseEntity<?> login(@Valid @RequestBody LoginDto loginDto) throws Exception {

        return userService.findOptionalByUsernameAndPassword(loginDto.getUsername(), loginDto.getPassword())
                .map(user -> {
                    // 1. 升级密码编码
                    userService.upgradePasswordEncodingIfNeeded(user, loginDto.getPassword());
                    if (!user.isEnabled()) {
                        throw new UserNotEnabledProblem();
                    }
                    if (!user.isAccountNonLocked()) {
                        throw new UserAccountLockedProblem();
                    }
                    if (!user.isAccountNonExpired()) {
                        throw new UserAccountExpiredProblem();
                    }

                    // 3. 判断usingMfa = false ===》 直接返回token
                    //    // 不使用多因子认证
                    if (!user.isUsingMfa()) {
                        return ResponseEntity.ok().body(userService.login(user));
                    }

                    // 4. 使用多因子认证
                    val mfaId = userCacheService.cacheUser(user);
                    return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                            .header("X-Authenticate", "mfa", "realm=" + mfaId)
                            .build();

                })
                .orElseThrow(() -> new BadCredentialsException("用户名或密码错误"))
                ;

    }

    @PostMapping("/token/refresh")
    public Auth refreshToken(
            @RequestHeader(name = "Authorization") String authorization,
            @RequestParam String refreshToken
    ) throws Exception {

        String PREFIX = "Bearer";

        String accessToken = authorization.replace(PREFIX, "");

        if (jwtUtil.validateRefreshToken(refreshToken) && jwtUtil.validateWithoutExpiration(accessToken)) {
            String token = jwtUtil.buildAccessTokenWithRefreshToken(accessToken);
            return new Auth(
                    jwtUtil.buildAccessTokenWithRefreshToken(refreshToken),
                    refreshToken
            );
        }

        throw new AccessDeniedException("访问被拒绝");
    }

    @PutMapping("/totp")
    public void sendTotp(@Valid @RequestBody SendTotpDto sendTotpDto) {

        Optional<Pair<User, String>> userStringPair = userCacheService.retrieveUser(sendTotpDto.getMfaId()).flatMap(user -> userService.createTotp(user).map(code -> Pair.of(user, code)));

        OptionalEx.ifPresent(userStringPair, pair -> {
            log.debug("totp: {}", pair.getSecond());
            if (sendTotpDto.getMfaType() == MfaType.SMS) {
                smsService.send(pair.getFirst().getMobile(), pair.getSecond());
            } else {
                emailService.sendEmail(pair.getFirst().getEmail(), pair.getSecond());
            }
        }).orElse(() -> {
            throw new InvalidTotpProblem();
        });
    }

    @PostMapping("/totp")
    public Auth verifyTotp(@Valid @RequestBody TotpVerificationDto totpVerificationDto) {
        return userCacheService.verifyTotp(totpVerificationDto.getMfaId(), totpVerificationDto.getCode())
                .map(User::getUsername)
                .flatMap(userService::findOptionalByUsername)
                .map(userService::loginWithTotp)
                .orElseThrow(InvalidTotpProblem::new);
    }

}
